package controller.thread_pool_solver;

import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Random;

import data.Solution;
import data.Task;

import user_interface.FBUserInterface;

import model.SimpleModel;

public class ThreadPool implements Runnable {
	// Main data
	private final SimpleModel model;
	private final FBUserInterface user_interface;
	private boolean calc_station = false;
	private boolean stop_calc_station = false;
	private boolean pause_station = false;
	// Task data
	private final Random random = new Random(System.currentTimeMillis());	
	private int randomSwappingNumber = 1;
	private int requestRandomSwappingNumber = randomSwappingNumber;
	private float swappingProportion = 0.2f;	
	private float requestSwappingProportion = swappingProportion;
	private int tabooListLength = 0;
	private int requestTabooListLength = tabooListLength;
	private int maxIteration = 10000;
	private int requestMaxIteration = maxIteration;
	private int neighborhoodSize = 5;
	private int requestNeighborhoodSize = neighborhoodSize;
	// Mutex
	private final Object general_mutex = new Object();
	/**
	 * Constructor.
	 */
	public ThreadPool(SimpleModel model, FBUserInterface userInterface) {
		this.model = model;
		this.user_interface = userInterface;
	}
	
	public void calculate(int newMaxIteration, int newNeighborhoodSize, int newTabooListLength, int newRandomSwappingNumber, float newSwappingProportion) {
		synchronized (general_mutex) {
			if(calc_station) 
				stop_calc_station = true;
			calc_station = true;
			
			requestMaxIteration = newMaxIteration;
			requestNeighborhoodSize = newNeighborhoodSize;
			requestTabooListLength = newTabooListLength;
			requestSwappingProportion = newSwappingProportion;
			requestRandomSwappingNumber = newRandomSwappingNumber;
		}
	}
	
	public void stop() {
		synchronized (general_mutex) {
			if(calc_station) 
				stop_calc_station = true;
			calc_station = false;
			pause_station = false;
		}		
	}
	
	public void pause(boolean state) {
		synchronized (general_mutex) {
			pause_station = state;
		}
	}
	
	@Override
	public void run() {
		while(true) {
			boolean local_calc_station = false;
			synchronized (general_mutex) {
				stop_calc_station = false;
				if(calc_station) {
					local_calc_station = true;
				}
			}
			if(local_calc_station) {
				calc();
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException ex) {
				// TODO
			}
		}
	}
//=============================================================================
//------------PRIVATE METHODS--------------------------------------------------
	private void calc() {
		Task task = model.getCurrentTask();
		if(task == null) {
			user_interface.print2console("Please, open file");
			return;
		}
		
		maxIteration = requestMaxIteration;
		neighborhoodSize = requestNeighborhoodSize;
		tabooListLength = requestTabooListLength;
		swappingProportion = requestSwappingProportion;
		randomSwappingNumber = requestRandomSwappingNumber;
		
		final int scenesNumber = task.getScenesNumber();
		final LimitedQueue<Integer> tabooList = new LimitedQueue<Integer>(tabooListLength);
		
		// generate initial solution
		int [] scenesOrder = generateInitialScenesOrder(task);

		// calculation solution
		if(scenesOrder == null || scenesOrder.length != scenesNumber)
			throw new IllegalStateException("invalid initial solution for corresponding task");
		float record = calculateCost(task, scenesOrder);
		Solution solution = new Solution(scenesOrder, record);
		user_interface.print2console("Initial solution: " + solution.toString());
		// generate diff
		final boolean [] diff = new boolean[scenesNumber];
		
		int emptyIteration = 0;
		for(int iterationNumber = 0 ; iterationNumber < maxIteration ; ++iterationNumber) {
			check_and_pause();
			if(check_and_stop()) return;
			
			long startEventTime = new Date().getTime(); // [DEBUG PERFORMANCE]
			
			int [][] neighborhood = generateNeighborhood(task, iterationNumber, solution, tabooList);
			if(neighborhood == null)
				throw new IllegalStateException("invalid neighborhood");
			if(neighborhood.length == 0)
				continue;
			
			int nextSolutionIndex = -1;
			float bestCost = solution.getCost();
			for(int neighborhoodItemIndex = 0 ; neighborhoodItemIndex < neighborhood.length ; ++neighborhoodItemIndex) {
				final float cost = calculateCost(task, neighborhood[neighborhoodItemIndex]);
				if(cost < bestCost) {
					bestCost = cost;
					nextSolutionIndex = neighborhoodItemIndex;
				}
			}
			
			if(nextSolutionIndex >= 0) {
				emptyIteration = 0;
				getChanges(solution.getScenesOrder(), neighborhood[nextSolutionIndex], diff);
				solution = new Solution(neighborhood[nextSolutionIndex], bestCost);
				user_interface.print2console("Find better solution: " + solution.toString());
				user_interface.setSolution(solution);
				for(int i = 0 ; i < scenesNumber ; ++i)
					if(diff[i])
						tabooList.offer(i);
			} else {
				emptyIteration++;
			}
			if(emptyIteration > 5) {
				emptyIteration = 0;
				tabooList.clear();
			}
				
			long finishEventTime = new Date().getTime(); // [DEBUG PERFORMANCE]
			user_interface.print2console((((iterationNumber + 1) / (float)maxIteration) * 100) + "% completed" + "\n  Time = " + ((finishEventTime - startEventTime) / 1000.0) + " sec\n");
		}
		
		user_interface.print2console("Task complete: \n  Solution: " + solution.toString());
		
		// set calc_station to false
		synchronized (general_mutex) {
			if(!stop_calc_station)
				calc_station = false;
		}		
	}	

	/**
	 * Generates the first solution for task. By default sets scenes in order of their indexes.
	 * @param task
	 */
	private int [] generateInitialScenesOrder(Task task) {
		final int[] initialSolution = new int[task.getScenesNumber()];
		for(int i = 0 ; i < initialSolution.length ; ++i)
			initialSolution[i] = i;
		return initialSolution;
	}
	
	/**
	 * Called before each iteration of solving. Shows if we should stop solving for specified task with specified number of iterations and solution.
	 * By default returns true after fixed number of iterations.
	 * @param task
	 * @param iterationNumber
	 * @param currentSolution
	 * @return true, if we should stop solving, otherwise - false 
	 */
//	private boolean stopSolving(Task task, int iterationNumber, Solution currentSolution) {
//		return iterationNumber > maxIteration;
//	}
	
	/**
	 * Called in the begin of each iteration of solving. Generates neighborhood for specified task with specified number of iterations and solution.
	 * Neighborhood can be empty.
	 * @param iterationNumber
	 * @param currentSolution
	 * @return
	 */
	private int [][] generateNeighborhood(Task task, int iterationNumber, Solution currentSolution, Collection<Integer> tabooList) {
		tabooList = new HashSet<Integer>(tabooList);//to speedup checking
		final int scenesNumber = task.getScenesNumber();
		final int [][] neighborhood = new int [neighborhoodSize][scenesNumber];
		final int swappingNumber = Math.min(scenesNumber - tabooListLength,
				Math.max(randomSwappingNumber, (int)(scenesNumber * swappingProportion + 0.5)));
		for(int neighborhoodItemIndex = 0 ; neighborhoodItemIndex < neighborhoodSize ; ++neighborhoodItemIndex) {
			final int [] neighborhoodItem = neighborhood[neighborhoodItemIndex];
			System.arraycopy(currentSolution.getScenesOrder(), 0, neighborhoodItem, 0, scenesNumber);
			if(scenesNumber > 1) {
				for(int i = 0 ; i < swappingNumber ; ++i) {
					int firstIndex = random.nextInt(scenesNumber);
					while(tabooList.contains(firstIndex))
						firstIndex = (++firstIndex) %scenesNumber;
					final int distanse = random.nextInt(scenesNumber - 1) + 1;
					int secondIndex = (firstIndex + distanse) % scenesNumber;
					while(secondIndex == firstIndex || tabooList.contains(secondIndex))
						secondIndex = (++secondIndex) %scenesNumber;
					final int tmp = neighborhoodItem[firstIndex];
					neighborhoodItem[firstIndex] = neighborhoodItem[secondIndex];
					neighborhoodItem[secondIndex] = tmp;
				}
			}
// It's make infinity loop
			// check on clones
//			for(int i = 0; i < neighborhoodItemIndex; i++) {
//				int j = 0;
//				for(j = 0; j < neighborhoodItem.length; j++) {
//					if(neighborhood[i][j] != neighborhoodItem[j]) {
//						break;
//					}
//				}
//				if(j == neighborhoodItem.length) {
//					neighborhoodItemIndex--;
//					break;
//				}
//			}
		}
		return neighborhood;
	}
	
	private float calculateCost(Task task, int [] scenesOrder) {
		float cost = 0f;
		final float [] actorsSalary = task.getActorsSalary();
		final int actorsNumber = task.getActorsNumber();
		final int scenesNumber = task.getScenesNumber();
		final boolean [][] actorsScenesRelation = task.getActorsScenesRelation();
		for(int actorIndex = 0 ; actorIndex < actorsNumber ; ++actorIndex) {
			int firstSceneDay = scenesNumber;
			int lastSceneDay = 0;
			for(int sceneIndex = 0 ; sceneIndex < scenesNumber ; ++sceneIndex) {
				if(actorsScenesRelation[actorIndex][sceneIndex]) {
					final int sceneDay = scenesOrder[sceneIndex];
					firstSceneDay = Math.min(firstSceneDay, sceneDay);
					lastSceneDay = Math.max(lastSceneDay, sceneDay);
				}
			}
			if(lastSceneDay >= firstSceneDay)
				cost += (lastSceneDay - firstSceneDay + 1) * actorsSalary[actorIndex];
		}
		return cost;
	}
	
	private static int getChanges(int [] order1, int [] order2, boolean [] diff) {
		final int length = order1.length;
		
		int number = 0;
		for(int i = 0 ; i < length ; ++i) {
			diff[i] = order1[i] != order2[i];
			if(diff[i])
				++number;
		}
		//System.out.println(Arrays.toString(diff));
		return number;
	}
	
	private void check_and_pause() {
		while(true) {
			synchronized (general_mutex) {
				if(!pause_station)
					break;
			}
			
			try {
				Thread.sleep(100);
			} catch (InterruptedException ex) {
				// TODO
			}
		}
	}
	
	private boolean check_and_stop() {
		synchronized (general_mutex) {
			if(stop_calc_station)
				return true;
			return false;
		}
	}
}
